package rest

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"
	"v8s-api/internal"
	"v8s-api/internal/configuration"
	"v8s-api/internal/db"
	"v8s/logger"

	"v8s-api/rest-api/target/generated-sources"

	"github.com/labstack/echo/v4"

	"github.com/xeipuuv/gojsonschema"

	"go.uber.org/zap"
)

type (
	ResourceId struct {
		ProjectID generated.ProjectId
		TaskID    string
	}

	Handlers struct {
		log *zap.SugaredLogger

		repository *db.ProjectsRepository
		features   configuration.FeaturesProperties
	}
)

func NewHandlers(repository *db.ProjectsRepository, properties configuration.FeaturesProperties) *Handlers {
	return &Handlers{
		log: logger.Logger().Named("rest-handlers").Sugar(),

		repository: repository,
		features:   properties,
	}
}

func (h *Handlers) GetComposeSchema(ctx echo.Context) error {
	properties := configuration.ReadProperties()
	schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("%s/compose-schema", properties.V8SController.Host))

	obj, err := schemaLoader.LoadJSON()
	if err != nil {
		return ctx.JSON(http.StatusUnprocessableEntity, generated.ErrorDto{
			Code:    "SCHEMA_ERROR",
			Message: fmt.Sprintf("failed to get schema from controller: %s", err),
		})
	}
	return ctx.JSON(http.StatusOK, obj)
}

func (h *Handlers) HealthCheckHealthGet(ctx echo.Context) error {
	err := h.repository.IsAlive(ctx.Request().Context())
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "UNHEALTHY",
			Message: fmt.Sprintf("repository is unhealthy: %s", err),
		})
	}

	return ctx.NoContent(http.StatusOK)
}

func (h *Handlers) ListTasks(ctx echo.Context) error {
	tasks, err := h.repository.FindTasks(ctx.Request().Context())
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Message: fmt.Sprintf("failed to get tasks: %s", err),
		})
	}

	items := make([]generated.Task, 0, len(tasks))

	for _, task := range tasks {
		current := task

		item, err := convert(task)
		if err != nil {
			return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
				Message: fmt.Sprintf("failed to convert task '%s': %s", current.ID, err),
			})
		}

		items = append(items, item)
	}

	return ctx.JSON(http.StatusOK, &generated.SearchResponse{
		Items: items,
		Size:  len(items),
	})
}

func (h *Handlers) CreateTask(ctx echo.Context) error {
	now := time.Now()
	request := &generated.CreateTaskRequest{}

	if err := ctx.Bind(request); err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("failed to parse request: %s", err),
		})
	}

	rn, err := internal.ParseResourceNameFromAPI(request.ResourceName)
	if err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("failed to parse resource name: %s", err),
		})
	}

	meta := map[string]interface{}{}
	if request.Metadata != nil {
		meta = *request.Metadata
	}

	err = isValid(request.Compose)
	if err != nil {
		return ctx.JSON(http.StatusUnprocessableEntity, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("compose file is not valid: %s", err),
		})
	}

	compose := map[string]interface{}{}
	if err := json.Unmarshal([]byte(request.Compose), &compose); err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("failed to unmarshall compose '%s': %s", request.Compose, err),
		})
	}

	taskToCreate := db.Task{
		ID:         internal.GenerateTaskID(),
		RN:         rn,
		TN:         db.TaskName(request.TaskName),
		Metadata:   meta,
		Compose:    compose,
		CreatedAt:  now,
		ModifiedAt: now,
		Status:     db.TaskStatusCreationRequest,
		LogStatus:  db.PatchRequstLoggingStatus,
	}

	result, err := h.repository.CreateTask(ctx.Request().Context(), internal.ParseProjectIDFromAPI(request.Project), taskToCreate)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("failed to create task: %s", err),
		})
	}

	loggingUser := rn.ToIsolationContext().StringWithDelimeter('-')

	err = h.repository.CreateCredentials(ctx.Request().Context(), internal.ParseProjectIDFromAPI(request.Project), loggingUser)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("failed to update credentials for Opensearch: %s", err),
		})
	}

	task, err := convert(result)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("failed to convert task: %s", err),
		})
	}

	return ctx.JSON(http.StatusOK, task)
}

func (h *Handlers) SearchTasks(ctx echo.Context) error {
	request := &generated.SearchRequest{}
	if err := ctx.Bind(request); err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "SEARCH_ERROR",
			Message: fmt.Sprintf("failed to parse search-request: %s", err),
		})
	}

	meta := map[string]interface{}{}

	if request.Metadata != nil {
		meta = *request.Metadata
	}

	tasks, err := h.repository.FindTasksUpdatedAfter(ctx.Request().Context(), *request.UpdatedAfter, meta, *request.Limit)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "SEARCH_ERROR",
			Message: fmt.Sprintf("failed to search tasks updated after '%s' with meta '%v': %s", *request.UpdatedAfter, meta, err),
		})
	}

	items := make([]generated.Task, 0, len(tasks))

	for _, task := range tasks {
		current := task

		item, err := convert(current)
		if err != nil {
			return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
				Code:    "SEARCH_ERROR",
				Message: fmt.Sprintf("failed to convert task '%s': %s", task.RN, err),
			})
		}

		items = append(items, item)
	}

	return ctx.JSON(http.StatusOK, generated.SearchResponse{
		Items: items,
		Size:  len(items),
	})
}

func (h *Handlers) DeleteTask(ctx echo.Context, resourceName generated.ResourceName) error {
	rn, err := internal.ParseResourceNameFromAPI(resourceName)
	if err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "DELETE_FAILED",
			Message: fmt.Sprintf("failed to parse rn: %s", err),
		})
	}

	if err := h.repository.UpdateTaskStatus(ctx.Request().Context(), rn, db.TaskStatusDeletionRequest, []db.TaskStatus{}); err != nil {
		if errors.Is(err, db.TaskIsAbsent) {
			return ctx.JSON(http.StatusNotFound, generated.ErrorDto{
				Code:    "DELETE_FAILED",
				Message: fmt.Sprintf("failed to delete task '%s': %s", rn, err),
			})
		}

		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "DELETE_FAILED",
			Message: fmt.Sprintf("failed to delete task '%s': %s", rn, err),
		})
	}

	return ctx.NoContent(http.StatusNoContent)
}

func (h *Handlers) GetTask(ctx echo.Context, resourceName generated.ResourceName) error {
	rn, err := internal.ParseResourceNameFromAPI(resourceName)
	if err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "GET_ERROR",
			Message: fmt.Sprintf("failed to parse rn: %s", err),
		})
	}

	task, exists, err := h.repository.GetTask(ctx.Request().Context(), rn)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "GET_ERROR",
			Message: fmt.Sprintf("failed to obtain task '%s': %s", rn, err),
		})
	}

	if !exists {
		return ctx.JSON(http.StatusNotFound, generated.ErrorDto{
			Code:    "GET_ERROR",
			Message: fmt.Sprintf("task '%s' not found", rn),
		})
	}

	response, err := convert(task)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "GET_ERROR",
			Message: fmt.Sprintf("failed to convert task '%s': %s", rn, err),
		})
	}

	return ctx.JSON(http.StatusOK, response)
}

func (h *Handlers) UpdateTask(ctx echo.Context, resourceName generated.ResourceName) error {
	rn, err := internal.ParseResourceNameFromAPI(resourceName)
	if err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "UPDATE_ERROR",
			Message: fmt.Sprintf("failed to parse rn: %s", err),
		})
	}

	request := generated.UpdateTaskRequest{}
	if err := ctx.Bind(&request); err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "UPDATE_ERROR",
			Message: fmt.Sprintf("failed to parse request: %s", err),
		})
	}

	meta := map[string]interface{}{}
	if request.Metadata != nil {
		meta = *request.Metadata
	}

	err = isValid(request.Compose)
	if err != nil {
		return ctx.JSON(http.StatusUnprocessableEntity, generated.ErrorDto{
			Code:    "CREATION_ERROR",
			Message: fmt.Sprintf("compose file is not valid: %s", err),
		})
	}

	compose := map[string]interface{}{}
	if err := json.Unmarshal([]byte(request.Compose), &compose); err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "UPDATE_ERROR",
			Message: fmt.Sprintf("failed to unmarshall task compose '%s': %s", rn, err),
		})
	}

	result, err := h.repository.UpdateTask(ctx.Request().Context(), rn, compose, meta)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "UPDATE_ERROR",
			Message: fmt.Sprintf("failed to update task '%s': %s", rn, err),
		})
	}

	response, err := convert(result)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "UPDATE_ERROR",
			Message: fmt.Sprintf("failed to convert task '%s': %s", rn, err),
		})
	}

	return ctx.JSON(http.StatusOK, response)
}

func (h *Handlers) ResumeTask(ctx echo.Context, resourceName generated.ResourceName) error {
	rn, err := internal.ParseResourceNameFromAPI(resourceName)
	if err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "RESUME_FAILED",
			Message: fmt.Sprintf("failed to parse rn: %s", resourceName),
		})
	}

	if err := h.repository.UpdateTaskStatus(ctx.Request().Context(), rn, db.TaskStatusResumeRequest, []db.TaskStatus{db.TaskStatusSuspended}); err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "RESUME_FAILED",
			Message: fmt.Sprintf("failed to resume task '%s': %s", rn, err),
		})
	}

	return ctx.NoContent(http.StatusNoContent)
}

func (h *Handlers) SuspendTask(ctx echo.Context, resourceName generated.ResourceName) error {
	rn, err := internal.ParseResourceNameFromAPI(resourceName)
	if err != nil {
		return ctx.JSON(http.StatusBadRequest, generated.ErrorDto{
			Code:    "SUSPEND_FAILED",
			Message: fmt.Sprintf("failed to parse rn: %s", resourceName),
		})
	}

	if err := h.repository.UpdateTaskStatus(ctx.Request().Context(), rn, db.TaskStatusSuspensionRequest, []db.TaskStatus{db.TaskStatusRunning}); err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "SUSPEND_FAILED",
			Message: fmt.Sprintf("failed to suspend task '%s': %s", rn, err),
		})
	}

	return ctx.NoContent(http.StatusNoContent)
}

func isValid(compose generated.Compose) error {
	properties := configuration.ReadProperties()
	schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("%s/compose-schema", properties.V8SController.Host))
	documentLoader := gojsonschema.NewStringLoader(string(compose))

	result, err := gojsonschema.Validate(schemaLoader, documentLoader)
	if err != nil {
		return fmt.Errorf("failed to validate: %s", err)
	}
	if result.Valid() {
		return nil
	} else {
		description := make([]string, 0)
		for _, desc := range result.Errors() {
			description = append(description, fmt.Sprintf("- %s", desc))
		}
		overallDesc := strings.Join(description, "\n")
		return fmt.Errorf("problems description:\n, %s", overallDesc)
	}

}

func convert(input db.Task) (generated.Task, error) {
	rn := input.RN

	compose, err := json.Marshal(input.Compose)
	if err != nil {
		return generated.Task{}, fmt.Errorf("failed to marshall compose: %w", err)
	}

	info := make(map[string]generated.ClusterDetails)

	allClustersRunning := len(input.Clusters) > 0

	for cluster, clusterDetails := range input.Clusters {
		if clusterDetails.Status != db.ClusterStatusRunning {
			allClustersRunning = false
		}

		resources := make([]generated.Resource, 0, len(clusterDetails.Resources))
		clusterServicesInfos := make(map[string]generated.ServiceInfo)

		for _, resource := range clusterDetails.Resources {
			resources = append(resources, generated.Resource{
				Kind:   resource.Kind,
				Name:   resource.Name,
				Status: string(resource.Status),
			})
		}

		for composeService, composeInfo := range clusterDetails.Services {
			serviceInfo := generated.ServiceInfo{}
			serviceInfo.Endpoints = make([]string, 0, len(composeInfo.Endpoints))
			for _, ep := range composeInfo.Endpoints {
				//
				serviceInfo.Endpoints = append(serviceInfo.Endpoints, ep)

			}
			copy(serviceInfo.Endpoints, composeInfo.Endpoints)
			clusterServicesInfos[string(composeService)] = serviceInfo
		}

		info[string(cluster)] = generated.ClusterDetails{
			Resources: resources,
			Services: generated.ServiceDetails{
				AdditionalProperties: clusterServicesInfos,
			},
			Status: generated.ClusterStatus(clusterDetails.Status),
		}
	}

	status := generated.TaskStatus(input.Status)

	if !allClustersRunning && input.Status == db.TaskStatusRunning {
		status = generated.TaskStatus(db.TaskStatusCreationRequest)
	}

	return generated.Task{
		Compose: generated.Compose(compose),
		Clusters: generated.Clusters{
			AdditionalProperties: info,
		},
		CreatedAt:    input.CreatedAt,
		ModifiedAt:   input.ModifiedAt,
		Metadata:     &input.Metadata,
		Status:       status,
		TaskName:     generated.TaskName(input.TN),
		ResourceName: internal.ToAPI(rn),
	}, nil
}

func (h *Handlers) DeleteTasks(ctx echo.Context) error {
	if !h.features.DeleteAllTasksEnabled {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "FEATURE_IS_DISABLED",
			Message: "DeleteTasks is disabled",
		})
	}

	tasks, err := h.repository.FindActiveTasks(ctx.Request().Context())
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, generated.ErrorDto{
			Code:    "DELETE_TASKS",
			Message: fmt.Sprintf("failed to receive tasks: %s", err),
		})
	}

	for _, task := range tasks {
		rn := task.RN

		if err = h.repository.UpdateTaskStatus(ctx.Request().Context(), rn, db.TaskStatusDeletionRequest, []db.TaskStatus{}); err != nil {
			h.log.Errorf("failed to delete task '%s': %s", rn, err)
			continue
		}
	}

	return ctx.NoContent(http.StatusNoContent)
}
